Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a header type field #4993

Merged
merged 13 commits into from
Aug 13, 2018
Merged

Add a header type field #4993

merged 13 commits into from
Aug 13, 2018

Conversation

tyrannosaurus-becks
Copy link
Contributor

@tyrannosaurus-becks tyrannosaurus-becks commented Jul 25, 2018

This field type came out of discussion here.

This adds a header type field. Under the hood, that's a map[string][]string which makes sense since one header key can have multiple values.

Once it's added, I can immediately use it in Alibaba creds (currently in development), and we could optionally retrofit AWS creds as well.

Here's how this field feels from the CLI:

  • vault write auth/somewhere/foo headers='hello:world'
  • vault write auth/somewhere/foo headers='hello:world' headers='fizz:buzz'
  • vault write auth/somewhere/foo headers='hello:world' headers='hello:monde'
  • vault write auth/somewhere/foo headers='eyJDb250ZW50LUxlbmd0aCI6IFsiNDMiXSwgIlVzZXItQWdlbnQiOiBbImF3cy1zZGstZ28vMS40LjEyIChnbzEuNy4xOyBsaW51eDsgYW1kNjQpIl0sICJYLVZhdWx0LUFXU0lBTS1TZXJ2ZXItSWQiOiBbInZhdWx0LmV4YW1wbGUuY29tIl0sICJYLUFtei1EYXRlIjogWyIyMDE2MDkzMFQwNDMxMjFaIl0sICJDb250ZW50LVR5cGUiOiBbImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZDsgY2hhcnNldD11dGYtOCJdLCAiQXV0aG9yaXphdGlvbiI6IFsiQVdTNC1ITUFDLVNIQTI1NiBDcmVkZW50aWFsPWZvby8yMDE2MDkzMC91cy1lYXN0LTEvc3RzL2F3czRfcmVxdWVzdCwgU2lnbmVkSGVhZGVycz1jb250ZW50LWxlbmd0aDtjb250ZW50LXR5cGU7aG9zdDt4LWFtei1kYXRlO3gtdmF1bHQtc2VydmVyLCBTaWduYXR1cmU9YTY5ZmQ3NTBhMzQ0NWM0ZTU1M2UxYjNlNzlkM2RhOTBlZWY1NDA0N2YxZWI0ZWZlOGZmYmM5YzQyOGMyNjU1YiJdfQ=='
  • vault write auth/somewhere/foo headers='{"hello":"world","bonjour":["monde","dieu"]}' (with proper bash escaping)

That's for a schema field named headers that is of TypeHeader.

"foo": {Type: TypeHeader},
},
map[string]interface{}{
"foo": []interface{}{"key1=value1", "key2=value2", "key3=1"},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if a header key name is repeated? I would expect it to end up with a list of values but I don't see a test for that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, if you provide a single value, I believe it will come in as a string interface and not a list. See TypeCommaStringSlice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I'll check out TypeCommaStringSlice.

On the double-key thing, I didn't add a test case for that because of the format of the test cases being a map, which obviously doesn't allow the same key multiple times. I'll break out a separate test for that.

Copy link
Contributor

@chrishoffman chrishoffman Jul 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I would expect CLI params of key1=value1 key1=value2 to result in:

map[string]interface{}{
    "key1": []interface{}{"value1", "value2"}
}

... or something like that :).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see what you're saying. 👍

"foo": {Type: TypeHeader},
},
map[string]interface{}{
"foo": map[string][]string{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the JSON Unmarshaller will return this as a map[string][]interface{}, can you test some non-string value and make sure you get what you expect.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

Copy link
Contributor Author

@tyrannosaurus-becks tyrannosaurus-becks Jul 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was a good test!

I ran Vault in dev mode and made a little path that took TypeHeader for a field named headers. Then I used the following command:

vault write auth/somewhere/foo headers='hello:world' headers='hello:true' headers="fizz:1"

And it actually parsed both the faux bool and the faux int field with no error, and they came in as strings, which is what I would want for a header.

screenshot from 2018-07-27 10-03-35

// canonicalize all the keys.
result := http.Header{}
for k, slice := range mapResult {
canonicalKey := textproto.CanonicalMIMEHeaderKey(k)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to do this step? It seems to me you would want to preserve formatting and any comparisons you would retrieve the header using the Get function and do any normalized text compares there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's definitely weird.

The reason I do it is because if you send in a header of "hello:world" and then later do header.Get("hello"), it doesn't find "world". That's because under the hood, the Get method first converts "hello" to "Hello" and then looks for it in the map.

I thought this was a really weird behavior, and tried to think through how to best achieve what someone would expect. I thought about, instead of using an http.Header, maybe I should just use a straight-out map. But then, people are sending in headers so maybe they would expect that kind of behavior.

In the end, I did this because I thought from a usability perspective it might be the best option. But I'm open to other approaches because it's a tough call.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am relatively certain that colons are illegal in header key values, which would explain this behavior.

Copy link
Contributor Author

@tyrannosaurus-becks tyrannosaurus-becks Jul 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if I explained that well LOL. In the example of "hello:world" I was thinking of "hello" as the key and "world" as the value. Probably would have been a better example if I'd done JSON or something.

I was trying to say that if you give it a lower-case key of hello, then later try to pull out a lower-case hello, it finds nothing. And it's because under the hood, it converts it to Title Case before searching for it in the map, so it's looking for "Hello" while it's sitting in the map as "hello", and thus misses it. It converts it to Title Case using that canonical method.

So by also calling that canonical method on keys on the way in, it also Title Cases the keys in the map so they'll have hits. This is definitely a trade-off because, while you'll now have case-insensitive hits using the header.Get("hello") method, if you need to pull headers out like this: header["hello"] it won't work anymore. You'd use the Get method for headers that you know only have one value, and the map-key-accessing method for headers that may have multiple values.

Ultimately, I'm honestly not super in love with the magic the http.Header type is doing because I think it's actually pretty weird. This is why I'm tempted to just turn it into a map[string][]string, and to do away with the canonical stuff, so it's way more evident and straight-forward how to use it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I too struggled with this type for whatever reason, it is challenging to use. Maybe you're doing a little too much sanitation and you just need to pass through what they give you and serialize it directly to disk when necessary. Here is the code I came up with dealing with headers, config, and output. https://github.com/hashicorp/vault/blob/master/vault/ui.go

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably you shouldn't do any sanitization at all -- you should get it to the correct type and just copy it over verbatim. There's no need for you to do any textproto stuff when Header is doing it anyways.

@jefferai
Copy link
Member

I don't know if there are any characters that are not valid in a header value, but if so it would be cool to be able to parse a pure string too. Thinking about thngs like #4982; this could move to parseutil (it probably should anyways) and be accessible from the API/CLI.

@tyrannosaurus-becks
Copy link
Contributor Author

@jefferai I read through #4982 and I'm not sure what you mean. Can you clarify?

Are you thinking it would be cool to also parse the headers to validate them? If so I could add logic mirroring RFC 7230.

@jefferai
Copy link
Member

Ultimately, I'm honestly not super in love with the magic the http.Header type is doing because I think it's actually pretty weird. This is why I'm tempted to just turn it into a map[string][]string, and to do away with the canonical stuff, so it's way more evident and straight-forward how to use it.

It does magic to work around the exact kind of issue you're running into by trying to access the map directly. Why not just always use the http.Header methods?

@jefferai
Copy link
Member

I read through #4982 and I'm not sure what you mean. Can you clarify?

What I mean is that the processing logic, if in parseutil, makes it easier to potentially share with other needs, such as if we build in arbitrary header support into CLI/API for requests. That's why we moved various other processing bits like TypeDurationSecond into parseutil.

Are you thinking it would be cool to also parse the headers to validate them? If so I could add logic mirroring RFC 7230.

No, I think using http.Header validates them for you. And if not, I'm fine with "buyer beware"

@tyrannosaurus-becks
Copy link
Contributor Author

tyrannosaurus-becks commented Jul 27, 2018

Why not just always use the http.Header methods?

I think this is a waaaay better explanation of what happens using the http.Header methods. IMHO the problem is that it's missing any method for getting an array of values that handles using the correctly-cased header key. Another option might even be writing a wrapper for http.Header that simply adds a method like that.

This might better illustrate what I'm getting at: https://play.golang.org/p/ohI4WuIEwUE

@jefferai
Copy link
Member

If you're accessing the map directly just use textproto to canonicalize it, problem solved!

@tyrannosaurus-becks
Copy link
Contributor Author

tyrannosaurus-becks commented Jul 27, 2018

@jefferai I totally agree that's how people should do it, but would they know to, or would it lead to bugs? Likewise, would they realize that under the hood it's a map[string][]string and they should look for all values and not just use the Get method?

Just want to have the discussion before we add this type and it gets reused in multiple places.

@jefferai
Copy link
Member

I guess I'm not really following the problem. If you want to provide utility functions I suggest creating a new type that embeds an http.Header, that seems useful.

To avoid bugs like this, we provide one more method. GetAll,
which returns all the values for a key.
*/
func NewHeader() Header {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably all go in parseutil if ParseHeader is in there (which it may be?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Np, I'll move it!

@tyrannosaurus-becks tyrannosaurus-becks modified the milestones: 0.11, 0.10.5 Jul 31, 2018
@tyrannosaurus-becks
Copy link
Contributor Author

Chatted with a couple folks about this, and we agreed we'd be OK with the risk of just using a raw http.Header field, so I've turned this back to that. Should be good to go!

case string:
header.Add(headerKey, typedHeader)
case []string:
for _, headerVal := range typedHeader {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this need to be range typedHeader.([]string)? Same with the case below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes and no. It's already converted to an []string here: switch typedHeader := headerValGroup.(type). The type is used in the switch below and it's actually cast to it when it's instantiated there on the left. Went off of this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right, I mixed up:

switch v.(type) {

which doesn't have that type with what you did here, which does. Great!

Copy link
Member

@jefferai jefferai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it looks good, although I'm surprised nothing is complaining when you're ranging over an interface{}.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants